﻿/************************************************************************

   Extended WPF Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://wpftoolkit.codeplex.com/license 

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. The commercial version of Extended WPF Toolkit also includes
   priority technical support, commercial updates, and many additional 
   useful WPF controls if you license Xceed Business Suite for WPF.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using Twi.COMMON.WPF.CtlExtended.Core.Utilities;

namespace Twi.COMMON.WPF.CtlExtended
{
    public class Calculator : System.Windows.Controls.Control
  {
    #region Members

    private ContentControl _buttonPanel;
    private bool _showNewNumber = true;
    private decimal _previousValue;
    private Operation _lastOperation = Operation.None;
    private CalculatorButtonType _lastButtonPressed;
    private readonly Dictionary<Button, DispatcherTimer> _timers = new Dictionary<Button, DispatcherTimer>();

    #endregion //Members

    #region Enumerations

    public enum CalculatorButtonType
    {
      Add,
      Back,
      Cancel,
      Clear,
      Decimal,
      Divide,
      Eight,
      Equal,
      Five,
      Four,
      Fraction,
      MAdd,
      MC,
      MR,
      MS,
      MSub,
      Multiply,
      Negate,
      Nine,
      None,
      One,
      Percent,
      Seven,
      Six,
      Sqrt,
      Subtract,
      Three,
      Two,
      Zero
    }

    public enum Operation
    {
      Add,
      Subtract,
      Divide,
      Multiply,
      Percent,
      Sqrt,
      Fraction,
      None,
      Clear,
      Negate
    }

    #endregion //Enumerations

    #region Properties

    public ICommand CalculaterButtonClickCommand
    {
      get;
      private set;
    }

    #region CalculatorButtonType

    public static readonly DependencyProperty CalculatorButtonTypeProperty = DependencyProperty.RegisterAttached( "CalculatorButtonType", typeof( CalculatorButtonType ), typeof( Calculator ), new UIPropertyMetadata( CalculatorButtonType.None, OnCalculatorButtonTypeChanged ) );
    public static CalculatorButtonType GetCalculatorButtonType( DependencyObject target )
    {
      return ( CalculatorButtonType )target.GetValue( CalculatorButtonTypeProperty );
    }
    public static void SetCalculatorButtonType( DependencyObject target, CalculatorButtonType value )
    {
      target.SetValue( CalculatorButtonTypeProperty, value );
    }
    private static void OnCalculatorButtonTypeChanged( DependencyObject o, DependencyPropertyChangedEventArgs e )
    {
      OnCalculatorButtonTypeChanged( o, ( CalculatorButtonType )e.OldValue, ( CalculatorButtonType )e.NewValue );
    }
    private static void OnCalculatorButtonTypeChanged( DependencyObject o, CalculatorButtonType oldValue, CalculatorButtonType newValue )
    {
      Button button = o as Button;
      button.CommandParameter = newValue;
      button.Content = CalculatorUtilities.GetCalculatorButtonContent( newValue );
    }

    #endregion //CalculatorButtonType

    #region DisplayText

    public static readonly DependencyProperty DisplayTextProperty = DependencyProperty.Register( "DisplayText", typeof( string ), typeof( Calculator ), new UIPropertyMetadata( "0", OnDisplayTextChanged ) );
    public string DisplayText
    {
      get
      {
        return ( string )GetValue( DisplayTextProperty );
      }
      set
      {
        SetValue( DisplayTextProperty, value );
      }
    }

    private static void OnDisplayTextChanged( DependencyObject o, DependencyPropertyChangedEventArgs e )
    {
      Calculator calculator = o as Calculator;
      if( calculator != null )
        calculator.OnDisplayTextChanged( ( string )e.OldValue, ( string )e.NewValue );
    }

    protected virtual void OnDisplayTextChanged( string oldValue, string newValue )
    {
      // TODO: Add your property changed side-effects. Descendants can override as well.
    }

    #endregion //DisplayText

    #region Memory

    public static readonly DependencyProperty MemoryProperty = DependencyProperty.Register( "Memory", typeof( decimal ), typeof( Calculator ), new UIPropertyMetadata( default( decimal ) ) );
    public decimal Memory
    {
      get
      {
        return ( decimal )GetValue( MemoryProperty );
      }
      set
      {
        SetValue( MemoryProperty, value );
      }
    }

    #endregion //Memory

    #region Precision

    public static readonly DependencyProperty PrecisionProperty = DependencyProperty.Register( "Precision", typeof( int ), typeof( Calculator ), new UIPropertyMetadata( 6 ) );
    public int Precision
    {
      get
      {
        return ( int )GetValue( PrecisionProperty );
      }
      set
      {
        SetValue( PrecisionProperty, value );
      }
    }

    #endregion //Precision

    #region Value

    public static readonly DependencyProperty ValueProperty = DependencyProperty.Register( "Value", typeof( decimal? ), typeof( Calculator ), new FrameworkPropertyMetadata( default( decimal ), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnValueChanged ) );
    public decimal? Value
    {
      get
      {
        return ( decimal? )GetValue( ValueProperty );
      }
      set
      {
        SetValue( ValueProperty, value );
      }
    }

    private static void OnValueChanged( DependencyObject o, DependencyPropertyChangedEventArgs e )
    {
      Calculator calculator = o as Calculator;
      if( calculator != null )
        calculator.OnValueChanged( ( decimal? )e.OldValue, ( decimal? )e.NewValue );
    }

    protected virtual void OnValueChanged( decimal? oldValue, decimal? newValue )
    {
      if( newValue.HasValue )
        DisplayText = newValue.ToString();
      else
        DisplayText = "0";

      RoutedPropertyChangedEventArgs<object> args = new RoutedPropertyChangedEventArgs<object>( oldValue, newValue );
      args.RoutedEvent = ValueChangedEvent;
      RaiseEvent( args );
    }

    #endregion //Value

    #endregion //Properties

    #region Constructors

    static Calculator()
    {
      DefaultStyleKeyProperty.OverrideMetadata( typeof( Calculator ), new FrameworkPropertyMetadata( typeof( Calculator ) ) );
    }

    public Calculator()
    {
      CommandBindings.Add( new CommandBinding( CalculatorCommands.CalculatorButtonClick, ExecuteCalculatorButtonClick ) );
      AddHandler( MouseDownEvent, new MouseButtonEventHandler( Calculator_OnMouseDown ), true );
    }

    #endregion //Constructors

    #region Base Class Overrides

    public override void OnApplyTemplate()
    {
      base.OnApplyTemplate();

      _buttonPanel = GetTemplateChild( "PART_CalculatorButtonPanel" ) as ContentControl;
    }

    protected override void OnTextInput( TextCompositionEventArgs e )
    {
      var buttonType = CalculatorUtilities.GetCalculatorButtonTypeFromText( e.Text );
      if( buttonType != CalculatorButtonType.None )
      {
        SimulateCalculatorButtonClick( buttonType );
        ProcessCalculatorButton( buttonType );
      }
    }

    #endregion //Base Class Overrides

    #region Event Handlers

    private void Calculator_OnMouseDown( object sender, MouseButtonEventArgs e )
    {
      if( !IsFocused )
      {
        Focus();
        e.Handled = true;
      }
    }

    void Timer_Tick( object sender, EventArgs e )
    {
      DispatcherTimer timer = ( DispatcherTimer )sender;
      timer.Stop();
      timer.Tick -= Timer_Tick;

      if( _timers.ContainsValue( timer ) )
      {
        var button = _timers.Where( x => x.Value == timer ).Select( x => x.Key ).FirstOrDefault();
        if( button != null )
        {
          VisualStateManager.GoToState( button, button.IsMouseOver ? "MouseOver" : "Normal", true );
          _timers.Remove( button );
        }
      }
    }

    #endregion //Event Handlers

    #region Methods

    private void Calculate()
    {
      if( _lastOperation == Operation.None )
        return;

      try
      {
        Value = Decimal.Round( CalculateValue( _lastOperation ), Precision );
      }
      catch
      {
        Value = null;
        DisplayText = "ERROR";
      }
    }

    private void Calculate( Operation newOperation )
    {
      if( !_showNewNumber )
        Calculate();

      _lastOperation = newOperation;
    }

    private void Calculate( Operation currentOperation, Operation newOperation )
    {
      _lastOperation = currentOperation;
      Calculate();
      _lastOperation = newOperation;
    }

    private decimal CalculateValue( Operation operation )
    {
      decimal newValue = decimal.Zero;
      decimal currentValue = CalculatorUtilities.ParseDecimal( DisplayText );

      switch( operation )
      {
        case Operation.Add:
          newValue = CalculatorUtilities.Add( _previousValue, currentValue );
          break;
        case Operation.Subtract:
          newValue = CalculatorUtilities.Subtract( _previousValue, currentValue );
          break;
        case Operation.Multiply:
          newValue = CalculatorUtilities.Multiply( _previousValue, currentValue );
          break;
        case Operation.Divide:
          newValue = CalculatorUtilities.Divide( _previousValue, currentValue );
          break;
        //case Operation.Percent:
        //    newValue = CalculatorUtilities.Percent(_previousValue, currentValue);
        //    break;
        case Operation.Sqrt:
          newValue = CalculatorUtilities.SquareRoot( currentValue );
          break;
        case Operation.Fraction:
          newValue = CalculatorUtilities.Fraction( currentValue );
          break;
        case Operation.Negate:
          newValue = CalculatorUtilities.Negate( currentValue );
          break;
        default:
          newValue = decimal.Zero;
          break;
      }

      return newValue;
    }

    void ProcessBackKey()
    {
      string displayText;
      if( DisplayText.Length > 1 && !( DisplayText.Length == 2 && DisplayText[ 0 ] == '-' ) )
      {
        displayText = DisplayText.Remove( DisplayText.Length - 1, 1 );
      }
      else
      {
        displayText = "0";
        _showNewNumber = true;
      }

      DisplayText = displayText;
    }

    private void ProcessCalculatorButton( CalculatorButtonType buttonType )
    {
      if( CalculatorUtilities.IsDigit( buttonType ) )
        ProcessDigitKey( buttonType );
      else if( ( CalculatorUtilities.IsMemory( buttonType ) ) )
        ProcessMemoryKey( buttonType );
      else
        ProcessOperationKey( buttonType );

      _lastButtonPressed = buttonType;
    }

    private void ProcessDigitKey( CalculatorButtonType buttonType )
    {
      if( _showNewNumber )
        DisplayText = CalculatorUtilities.GetCalculatorButtonContent( buttonType );
      else
        DisplayText += CalculatorUtilities.GetCalculatorButtonContent( buttonType );

      _showNewNumber = false;
    }

    private void ProcessMemoryKey( Calculator.CalculatorButtonType buttonType )
    {
      decimal currentValue = CalculatorUtilities.ParseDecimal( DisplayText );

      switch( buttonType )
      {
        case Calculator.CalculatorButtonType.MAdd:
          Memory += currentValue;
          break;
        case Calculator.CalculatorButtonType.MC:
          Memory = decimal.Zero;
          break;
        case Calculator.CalculatorButtonType.MR:
          DisplayText = Memory.ToString();
          break;
        case Calculator.CalculatorButtonType.MS:
          Memory = currentValue;
          break;
        case Calculator.CalculatorButtonType.MSub:
          Memory -= currentValue;
          break;
        default:
          break;
      }

      _showNewNumber = true;
    }

    private void ProcessOperationKey( CalculatorButtonType buttonType )
    {
      switch( buttonType )
      {
        case CalculatorButtonType.Add:
          Calculate( Operation.Add );
          break;
        case CalculatorButtonType.Subtract:
          Calculate( Operation.Subtract );
          break;
        case CalculatorButtonType.Multiply:
          Calculate( Operation.Multiply );
          break;
        case CalculatorButtonType.Divide:
          Calculate( Operation.Divide );
          break;
        case CalculatorButtonType.Percent:
          if( _lastOperation != Operation.None )
          {
            decimal currentValue = CalculatorUtilities.ParseDecimal( DisplayText );
            decimal newValue = CalculatorUtilities.Percent( _previousValue, currentValue );
            DisplayText = newValue.ToString();
          }
          else
          {
            DisplayText = "0";
            _showNewNumber = true;
          }
          return;
        case CalculatorButtonType.Sqrt:
          Calculate( Operation.Sqrt, Operation.None );
          break;
        case CalculatorButtonType.Fraction:
          Calculate( Operation.Fraction, Operation.None );
          break;
        case CalculatorButtonType.Negate:
          Calculate( Operation.Negate, Operation.None );
          break;
        case CalculatorButtonType.Equal:
          Calculate( Operation.None );
          break;
        case CalculatorButtonType.Clear:
          Calculate( Operation.Clear, Operation.None );
          DisplayText = Value.ToString();
          break;
        case CalculatorButtonType.Cancel:
          DisplayText = _previousValue.ToString();
          _lastOperation = Operation.None;
          _showNewNumber = true;
          return;
        case CalculatorButtonType.Back:
          ProcessBackKey();
          return;
        default:
          break;
      }

      Decimal.TryParse( DisplayText, out _previousValue );
      _showNewNumber = true;
    }

    private void SimulateCalculatorButtonClick( CalculatorButtonType buttonType )
    {
      var button = CalculatorUtilities.FindButtonByCalculatorButtonType( _buttonPanel, buttonType );
      if( button != null )
      {
        VisualStateManager.GoToState( button, "Pressed", true );
        DispatcherTimer timer;
        if( _timers.ContainsKey( button ) )
        {
          timer = _timers[ button ];
          timer.Stop();
        }
        else
        {
          timer = new DispatcherTimer();
          timer.Interval = TimeSpan.FromMilliseconds( 100 );
          timer.Tick += Timer_Tick;
          _timers.Add( button, timer );
        }

        timer.Start();
      }
    }

    #endregion //Methods

    #region Events

    //Due to a bug in Visual Studio, you cannot create event handlers for nullable args in XAML, so I have to use object instead.
    public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent( "ValueChanged", RoutingStrategy.Bubble, typeof( RoutedPropertyChangedEventHandler<object> ), typeof( Calculator ) );
    public event RoutedPropertyChangedEventHandler<object> ValueChanged
    {
      add
      {
        AddHandler( ValueChangedEvent, value );
      }
      remove
      {
        RemoveHandler( ValueChangedEvent, value );
      }
    }

    #endregion //Events

    #region Commands

    private void ExecuteCalculatorButtonClick( object sender, ExecutedRoutedEventArgs e )
    {
      var buttonType = ( CalculatorButtonType )e.Parameter;
      ProcessCalculatorButton( buttonType );
    }

    #endregion //Commands
  }
}
